﻿using System.Reflection.Emit;
using System;
using System.Reflection;
using ProxyViewModel.Commands;
using ProxyViewModel.Attributes;
using System.Collections.Generic;
using System.Linq;

namespace ProxyViewModel.Emitors {
	internal class PropertyOverrideEmitor : EmitorBase {
		public PropertyOverrideEmitor(TypeBuilder typeBuilder, Type operatable, IDictionary<string, object> parameters)
			: base(typeBuilder, operatable, parameters) { }

		protected override void EmitContent() {
			foreach (PropertyInfo info in ReflectionHelper.GetPublicVirtualProperties(Operatable)) {
				MethodInfo notifyPredicate = ReflectionHelper.GetMethodByName(Operatable,
					string.Format("NotifyPredicate{0}", info.Name));
				if (info.GetSetMethod() != null && info.GetGetMethod() != null) {
					OverrideSetMethod(info, TypeBuilder, info.Name, DoesMethodQulifyAsPredicate(notifyPredicate, info) ? notifyPredicate : null);
				}
			}
		}

		private bool DoesMethodQulifyAsPredicate(MethodInfo notifyPredicateInfo, PropertyInfo propertyInfo) {
			return notifyPredicateInfo != null &&
				notifyPredicateInfo.GetParameters().Length == 2 &&
				notifyPredicateInfo.GetParameters()[0].ParameterType == propertyInfo.PropertyType &&
				notifyPredicateInfo.GetParameters()[1].ParameterType == propertyInfo.PropertyType &&
				notifyPredicateInfo.ReturnType == typeof(bool);
		}

		private void OverrideSetMethod(PropertyInfo info, TypeBuilder typeBuilder, string propertyName, MethodInfo notifyPredicate) {
			MethodBuilder methodBuilder = typeBuilder.DefineMethod(string.Format("set_{0}", propertyName),
				MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual,
				null, new Type[] { info.PropertyType });

			ILGenerator il = methodBuilder.GetILGenerator();
			il.DeclareLocal(typeof(bool));
			if (notifyPredicate != null) {
				il.DeclareLocal(info.PropertyType);
				il.DeclareLocal(typeof(bool));
			}
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Call, Operatable.GetMethod(string.Format("get_{0}", propertyName)));
			LoadOpInequality(info.PropertyType, il);
			il.Emit(OpCodes.Ceq);
			il.Emit(OpCodes.Stloc_0);
			il.Emit(OpCodes.Ldloc_0);
			Label lbl1 = il.DefineLabel();
			il.Emit(OpCodes.Brtrue_S, lbl1);
			il.Emit(OpCodes.Nop);

			if (HasVlidators(info)) {
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldstr, info.Name);
				il.Emit(OpCodes.Ldarg_1);
				il.Emit(OpCodes.Call, GetParameter<MethodBuilder>(Parameters.ValidationFunctionKey));
				il.Emit(OpCodes.Stloc_0);
				il.Emit(OpCodes.Ldloc_0);
				il.Emit(OpCodes.Brtrue_S, lbl1);
				il.Emit(OpCodes.Nop);
			}

			if (notifyPredicate != null) {
				SetTemporaryVariable(il, propertyName, info.PropertyType);
			}
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Call, Operatable.GetMethod(string.Format("set_{0}", propertyName)));

			// raise command invalidation
			if (info.GetCustomAttributes(typeof(DonotInvalidateCommandsAttribute), true).Length == 0) {
				InvalidateCommands(il);
			}

			Label exitLabel = new Label();
			if (notifyPredicate != null) {
				// evaluat weather to raise propertychanged or not
				exitLabel = EvaluatePredicate(il, notifyPredicate);
			}

			// raise property change goes here
			InvokeRaisePropertyChanged(il, propertyName, GetParameter<MethodInfo>(Parameters.RaisePropertyChangedMethodKey));

			il.MarkLabel(lbl1);
			if (notifyPredicate != null) {
				il.MarkLabel(exitLabel);
			}
			il.Emit(OpCodes.Ret);
		}

		private bool HasVlidators(PropertyInfo info) {
			return info.GetCustomAttributes(true).OfType<ValidatorBaseAttribute>().Count() > 0;
		}

		public static void InvokeRaisePropertyChanged(ILGenerator il, string propertyName, MethodInfo raisePropertyChangedMethod) {
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldstr, propertyName);
			il.Emit(OpCodes.Call, raisePropertyChangedMethod);
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Nop);
		}

		private void InvalidateCommands(ILGenerator il) {
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Call, typeof(ProxyCommandManager).GetMethod("InvalidateRequerySuggested", BindingFlags.Public | BindingFlags.Static));
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Nop);
			if (CurrentParameters.ContainsKey(Parameters.InvalidateParentViewModelMethodKey)) {
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Call, GetParameter<MethodBuilder>(Parameters.InvalidateParentViewModelMethodKey));
				il.Emit(OpCodes.Nop);
				il.Emit(OpCodes.Nop);
			}
		}

		private void SetTemporaryVariable(ILGenerator il, string propertyName, Type propertyType) {
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Call, Operatable.GetMethod(string.Format("get_{0}", propertyName)));
			il.Emit(OpCodes.Stloc_1);
			il.Emit(OpCodes.Nop);
		}

		private Label EvaluatePredicate(ILGenerator il, MethodInfo notifyPredicate) {
			Label exitLabel = il.DefineLabel();
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Ldloc_1);
			il.Emit(OpCodes.Call, notifyPredicate);
			il.Emit(OpCodes.Ldc_I4_0);
			il.Emit(OpCodes.Ceq);
			il.Emit(OpCodes.Stloc_2);
			il.Emit(OpCodes.Ldloc_2);
			il.Emit(OpCodes.Brtrue_S, exitLabel);
			return exitLabel;
		}

		private void LoadOpInequality(Type propertyType, ILGenerator ilGenerator) {
			if (propertyType == typeof(decimal)) {
				ilGenerator.Emit(OpCodes.Call, typeof(decimal).GetMethod("op_Inequality"));
				ilGenerator.Emit(OpCodes.Ldc_I4_0);
			}
		}
	}
}
